Skip to content

Add PrecomputedInsulinInput for efficient multi-step prediction sweeps + parallelize glucose-effects#29

Open
ps2 wants to merge 6 commits into
tidepool-org:mainfrom
ps2:pr-a-precomputed-insulin
Open

Add PrecomputedInsulinInput for efficient multi-step prediction sweeps + parallelize glucose-effects#29
ps2 wants to merge 6 commits into
tidepool-org:mainfrom
ps2:pr-a-precomputed-insulin

Conversation

@ps2
Copy link
Copy Markdown
Collaborator

@ps2 ps2 commented May 19, 2026

Summary

Adds PrecomputedInsulinInput — a precomputed, per-dose annotated insulin-effect input designed for tools that run Loop's prediction repeatedly over the same data with varying ISF/sensitivity assumptions (e.g., evaluation tools doing per-step replay over multi-week windows).

Companion change: glucoseEffectsMidAbsorptionISF is parallelized via DispatchQueue.concurrentPerform since each delta-step's increment is independent until the final cumsum. Same behavior, ~5-10× faster on multi-core machines.

Commits

  1. Add PrecomputedInsulinInput for efficient multi-step prediction sweeps — main type + the prediction-input plumbing
  2. Refactor PrecomputedInsulinInput for explicit ISF-sweep pattern — separates per-dose annotation (one-time work) from per-step glucose-effect generation (per-call)
  3. Add sliced(from:to:) for per-step dose window slicing — slim the annotated set down to the window relevant to a single prediction call
  4. Expose dose-recommendation internals as public API — needed so external tools can compose dose decisions from a precomputed prediction
  5. Parallelize glucose-effects accumulation in InsulinMathDispatchQueue.concurrentPerform across the time-step axis

Why

Used by LoopEval (a closed-loop sim that runs Loop's prediction at every 5-min step across multi-week windows) where the same dose history is re-used with different sensitivity multipliers. Pre-annotating each dose's per-component contribution lets the per-step sweep reduce to O(active doses × ISF segments) without re-doing the dose-decomposition work.

Test plan

  • All existing tests pass (xcrun swift test)
  • Added PrecomputedInsulinInputTests.swift with 4 tests:
    • testPrecomputedAnnotationMatchesStandard — equivalence vs glucoseEffects for arbitrary dose histories
    • testISFSweepPattern — invariant: same dose history, different ISF multipliers → expected proportional response
    • testSlicedAnnotatedDosesMatchStandardsliced(from:to:) matches a fresh annotate() over the same window
    • testPrebuiltEffectsFastPathRunsWithoutError — smoke test for the pre-built-effects code path
  • Parallel glucoseEffectsMidAbsorptionISF produces bit-identical output to single-threaded path (covered by existing LoopAlgorithmTests.swift equivalence cases)

Bot and others added 6 commits May 19, 2026 10:48
Introduces PrecomputedInsulinInput and a new generatePrediction overload
that accepts pre-annotated dose data, enabling significant speedups for
historical back-testing / evaluation sweeps.

The key bottleneck in a dense prediction sweep is doses.annotated(with: basal),
which is O(doses × basalSegments) and was called from scratch at every step.
Between adjacent 5-min steps the dose list changes only at its edges; the
annotation of every dose in the middle is identical.

Changes:
- Sources/LoopAlgorithm/Insulin/PrecomputedInsulinInput.swift (new)
  PrecomputedInsulinInput struct holding pre-annotated doses and an optional
  pre-built insulinEffects timeline. Includes a convenience .build() factory.

- Sources/LoopAlgorithm/LoopAlgorithm.swift
  New generatePrediction(start:glucoseHistory:precomputedInsulin:carbEntries:
  sensitivity:carbRatio:...) overload. Skips annotated(with:) entirely;
  optionally skips glucoseEffects() when insulinEffects is pre-supplied.

- Sources/LoopAlgorithm/Glucose/GlucoseEffect.swift
  Add Sendable conformance (struct with value-type fields, safe).

- Tests/LoopAlgorithmTests/PrecomputedInsulinInputTests.swift (new)
  3 tests verifying the new overload produces output matching the standard
  path (bit-identical for annotation-only, count-identical + clinically
  equivalent for pre-built effects).

Expected speedup for a 7-day sweep at 5-min step (~2016 calls):
  annotation bypass alone: ~40-60% wall-clock reduction
  + effects cache (fixed ISF): additional ~20-30%
Split the API into two explicit steps so ISF sweeps pay annotation cost
exactly once across all multipliers:

  annotate(doses:basal:)          → ISF-independent, build once
  .withEffects(sensitivity:from:to:) → ISF-dependent, once per multiplier

Correct ISF sweep pattern:
  let base = PrecomputedInsulinInput.annotate(doses: doses, basal: basal)
  for multiplier in isfMultipliers {
    let input = base.withEffects(sensitivity: scale(sensitivity, by: multiplier))
    // run ~2016 steps with input — no annotation, no per-step glucoseEffects
  }

Cost breakdown for 10-multiplier × 7-day sweep (n≈2016 steps each):
  Before: annotated(with:) + glucoseEffects() called 20160× each
  After:  annotated(with:) called 1×, glucoseEffects() called 10×

Also adds testISFSweepPattern verifying bit-identical output across
multipliers [0.7, 0.8, ..., 1.3] vs the standard generatePrediction path.
Enables EvalCore to slice pre-annotated doses to the per-step lookback
window without re-annotating. Uses binary search on startDate + linear
filter on endDate (arrays are ~100-200 entries, linear endDate scan
is negligible).

Also cleans up the unused private partition helper (now only used by sliced).
Downstream callers (LoopEval bench engine) need to compute dose
recommendations from a forecast without going through the full run() API,
which re-computes insulin effects. Making insulinCorrection,
recommendTempBasal, and recommendAutomaticDose public lets them do
that efficiently using already-computed predictions.

Enables delivery-based ODR/UDR metrics in LoopEval that compare the
actual insulin Loop would deliver across two configurations.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Replace the sequential reduce loop with DispatchQueue.concurrentPerform
over per-step increments, then a final cumsum. Per-step contributions are
independent until the final summation, so this scales with available cores.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants